{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "LmfLXp5_bt-a",
    "tags": []
   },
   "source": [
    "# Knowledge Base Q&A Demo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "bbPzgYbrwbK2"
   },
   "source": [
    "\n",
    "This tutorial demonstrates how to use Qianfan API and ERNIE models to implement a basic knowledge base question and answer system.\n",
    "\n",
    "The system comprises two primary components: knowledge base construction and knowledge base-driven Q&A. By integrating text embedding with embedding-based retrieval technology, it delivers better answers derived from the knowledge base."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "IS2GUSP0_Eim"
   },
   "source": [
    "## 1. Environmental Setup\n",
    "Before starting, ensure your system meets these requirements:\n",
    "- Python version 3.10-3.12 is installed.\n",
    "- Ensure the following Python libraries are included: `openai`, `hashlib`, `json`, `numpy`, `textwrap`, `faiss`\n",
    "- Deploy [ERNIE-4.5](https://github.com/PaddlePaddle/FastDeploy) series model services and correctly configure the corresponding service address `host_url`\n",
    "- Set embedding model related parameters, including embedding model address `embedding_service_url`, model name `embedding_model`, key `qianfan_api_key`\n",
    "    - For the embedding models supported by Qianfan, please refer to [Qianfan Embedding Models](https://cloud.baidu.com/doc/qianfan-docs/s/Um8r1tpwy).After confirming `embedding_model`, you need to set the corresponding `embedding_dim` according to the model.\n",
    "    - You can log in to [Qianfan](https://console.bce.baidu.com/iam/#/iam/apikey/list) to create your API key.API keys are sensitive information and should be kept properly\n",
    "- Set the `test_file` text file for testing"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "id": "YD6urJjWGVDf"
   },
   "outputs": [],
   "source": [
    "# Model service address configuration\n",
    "host_url = \"http://localhost:port/v1\"\n",
    "\n",
    "# Embedding service configuration\n",
    "embedding_service_url = \"https://qianfan.baidubce.com/v2\"\n",
    "qianfan_api_key = \"bce-v3/xxx\"  # Replace with your real API key\n",
    "embedding_model = \"embedding-v1\"\n",
    "embedding_dim = 384  # Embedding dimension, adjust according to the model\n",
    "top_k = 3  # Number of retrieved texts returned per query\n",
    "model_api_key = \"api_key\" # the API key for model, which can be disregarded and replaced with any arbitrary value when using the model deployed locally.\n",
    "\n",
    "# Test file\n",
    "test_file = \"../data/coffee.txt\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.1. Install Dependencies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install openai numpy faiss-cpu"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Main Implementation Structure Overview\n",
    "\n",
    "- **Knowledge Base Construction**: When developing a knowledge base about question and answer system, building an embedding-based knowledge base is the core pre-work. It mainly includes two steps.\n",
    "\n",
    "    1) **Text Chunking Processing**: First, divide the original document into chunks, transform lengthy documents into knowledge units which is suitable for semantic retrieval.\n",
    "\n",
    "    2) **Text Chunk Embedding and Database Ingestion**: Transform text chunks into high-dimensional embeddings through embedding models. Establish associated storage between original texts and embedding data, forming an efficient retrievable \"text-embedding\" dual-index structure stored in the knowledge base.\n",
    "        \n",
    "    The construction of the knowledge base allows the system to quickly locate the most relevant text fragments by calculating the similarity between the query embedding and the knowledge base embeddings, providing a factual basis for the generation of accurate answers by the model, effectively solving the possible “hallucination” problems in the model.\n",
    "\n",
    "- **Knowledge Base-based Q&A**: Use the ERNIE models API to create a knowledge base Q&A system. The question and answer process mainly includes the following three steps.\n",
    "\n",
    "    1) **Retrieval Query Rewriting**: First, analyze whether the user's query needs obtain the latest information from the internet. When searching, rewrite the user's query to get the queries that be searched.\n",
    "\n",
    "    2) **Knowledge Base Retrieval**: Generate embeddings for the query, compare them with the text embeddings stored in the knowledge base, and obtain relevant text paragraphs.\n",
    "\n",
    "    3) **Generate the Final Answer**: The LLM synthesizes the final response by processing and consolidating relevant retrieved text passages."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "dD1lQx3Zr3S2"
   },
   "source": [
    "## 3. Knowledge Base Construction\n",
    "\n",
    "This example uses the `faiss` database to store and retrieve embeddings.\n",
    "\n",
    "### 3.1. Text Chunking Processing\n",
    "\n",
    "Suppose there is a document about coffee, you need to build an embeddings database for this document so that you can then retrieve relevant information based on the query.\n",
    "\n",
    "Before embedding text, you need to chunk the text. This example sets `chunk_size` to 512, and adopts a line-by-line segmentation strategy to retrieve more relevant text fragments in fine-grained size.\n",
    "- If the content of one line is less than 512, add the content of the next line; if it exceeds 512, look for the most recent punctuation point to truncate the line content."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "id": "XvLRIbpq4vNN"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，', '公平贸易咖啡与有机咖啡是一个不断扩大的市场。\\n传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。', '咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。', '同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\\n1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\\n1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。', '18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\\n在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\\n咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。']\n"
     ]
    }
   ],
   "source": [
    "def split_oversized_line(line: str, chunk_size: int) -> tuple:\n",
    "    PUNCTUATIONS = {\".\", \"。\", \"!\", \"！\", \"?\", \"？\", \",\", \"，\", \";\", \"；\", \":\", \"：\"}\n",
    "\n",
    "    if len(line) <= chunk_size:\n",
    "        return line, \"\"\n",
    "\n",
    "    # Search from chunk_size position backwards\n",
    "    split_pos = chunk_size\n",
    "    for i in range(chunk_size, 0, -1):\n",
    "        if line[i] in PUNCTUATIONS:\n",
    "            split_pos = i + 1  # Include punctuation\n",
    "            break\n",
    "\n",
    "    # Fallback to whitespace if no punctuation found\n",
    "    if split_pos == chunk_size:\n",
    "        split_pos = line.rfind(\" \", 0, chunk_size)\n",
    "        if split_pos == -1:\n",
    "            split_pos = chunk_size  # Hard split\n",
    "\n",
    "    return line[:split_pos], line[split_pos:]\n",
    "\n",
    "def split_text_into_chunks(text: str, chunk_size: int) -> list:\n",
    "    lines = [line.strip() for line in text.split(\"\\n\") if line.strip()]\n",
    "    chunks = []\n",
    "    current_chunk = []\n",
    "    current_length = 0\n",
    "\n",
    "    for line in lines:\n",
    "\n",
    "        # If adding this line would exceed chunk size (and we have content)\n",
    "        if current_length + len(line) > chunk_size and current_chunk:\n",
    "            chunks.append(\"\\n\".join(current_chunk))\n",
    "            current_chunk = []\n",
    "            current_length = 0\n",
    "\n",
    "        # Process oversized lines first\n",
    "        while len(line) > chunk_size:\n",
    "            head, line = split_oversized_line(line, chunk_size)\n",
    "            chunks.append(head)\n",
    "\n",
    "        # Add remaining line content\n",
    "        if line:\n",
    "            current_chunk.append(line)\n",
    "            current_length += len(line) + 1\n",
    "\n",
    "    if current_chunk:\n",
    "        chunks.append(\"\\n\".join(current_chunk))\n",
    "    return chunks\n",
    "\n",
    "with open(test_file, \"r\", encoding=\"utf-8\") as f:\n",
    "    test_text = f.read()\n",
    "\n",
    "segments = split_text_into_chunks(test_text, chunk_size=512)\n",
    "print(segments[:5])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "LHonPYEwStLB"
   },
   "source": [
    "### 3.2. Text Chunk Embedding and Database Ingestion\n",
    "\n",
    "#### 3.2.1. Embedding Generation Example\n",
    "\n",
    "This example uses Qianfan API to generate text embeddings.\n",
    "\n",
    "An embedding is a distributed numerical representation of text, composed of an array of floating-point numbers. This representation is highly useful for various natural language processing tasks. In this example, we will use embeddings to calculate the similarity between queries and knowledge base texts."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0.06902910023927689, -0.02064981311559677, -0.09815074503421783, 0.08861374109983444, -0.05157714709639549, -0.13549675047397614, 0.027453964576125145, -0.023358309641480446, -0.026020705699920654, -0.01652238890528679, -0.0017636652337387204, -0.08663472533226013, 0.060991719365119934, -0.001433665631338954, -0.07765280455350876, 0.010916685685515404, 0.0037388403434306383, -0.008097093552350998, -0.004761330783367157, -0.03509196266531944, 0.025797778740525246, -0.053582921624183655, 0.0033174229320138693, -0.04033001884818077, 0.03428077697753906, 0.006707459222525358, 0.05069943889975548, 0.08020070195198059, 0.02807926945388317, 0.012830601073801517, 0.005108212120831013, 0.0682472214102745, 0.08067227900028229, -0.08299519866704941, -0.041390180587768555, 0.06293655186891556, -0.022655190899968147, 0.000996474758721888, -0.026314564049243927, 0.08683788776397705, 0.02757883071899414, -0.0027709805872291327, 0.028281809762120247, -0.018270548433065414, 0.08003634959459305, -0.12196885049343109, -0.056998468935489655, -0.02333405241370201, -0.06551925092935562, 0.0315762422978878, 0.001037031877785921, 0.04430923983454704, 0.1054653599858284, 0.06728596985340118, -0.01829889416694641, -0.008648361079394817, -0.07413625717163086, -0.026104599237442017, 0.059346046298742294, -0.05114193260669708, -0.04633801430463791, 0.0364503338932991, 0.10136435180902481, -0.048290807753801346, 0.10299105942249298, 0.05649077519774437, -0.03151465207338333, 0.022651713341474533, 0.04111502692103386, -0.009610558860003948, 0.03487059473991394, -0.05934298038482666, 0.02765224501490593, 0.034416649490594864, -0.0202739629894495, 0.08460138738155365, -0.0083199767395854, 0.023836616426706314, 0.06835906207561493, -0.07559925317764282, 0.08442452549934387, 0.006683662533760071, 0.08673819154500961, 0.015553551726043224, -0.11905435472726822, -0.04445292428135872, 0.10648924112319946, 0.007692687679082155, -0.0026310172397643328, -0.027316149324178696, 0.011222605593502522, -0.08177153021097183, 0.07688300311565399, -0.012517046183347702, -0.10624496638774872, -0.07089240849018097, -0.04570743441581726, 0.026570936664938927, 0.0055390591733157635, -0.10976123809814453, 0.007593727204948664, 0.0798032358288765, -0.004682272672653198, 0.002999532036483288, 0.09999089688062668, -0.05949293449521065, -0.0798720270395279, -0.07874537259340286, 0.02925257571041584, 0.012559361755847931, -0.08104752749204636, 0.04633769765496254, -0.05368822440505028, 0.009231893345713615, 0.008213266730308533, 0.04279320687055588, 0.011830301955342293, -0.022767391055822372, 0.009919494390487671, 0.00734842661768198, -0.030985893681645393, 0.02011285535991192, -0.05481714755296707, 0.07407107204198837, -0.07009966671466827, 0.04820315167307854, -0.14042289555072784, -0.0025907987728714943, 0.008534450083971024, 0.06085103005170822, 0.08107241988182068, 0.051909416913986206, -0.04352433979511261, 0.037940531969070435, 0.02488063834607601, 0.0511770024895668, -0.018011318519711494, -0.06472807377576828, -0.05579017475247383, 0.07917359471321106, -0.05891445279121399, 0.08271721750497818, 0.04542738199234009, 0.00040362621075473726, -0.03726900741457939, 0.020076602697372437, 0.025510020554065704, -0.01853681169450283, -0.03148650750517845, -0.044669125229120255, -0.08029467612504959, 0.061398573219776154, -0.044040605425834656, -0.03548990190029144, -0.0766347348690033, 0.001127738389186561, 0.1083831936120987, -0.11496994644403458, -0.06664140522480011, 0.03932565823197365, 0.023469578474760056, -0.02688688039779663, 0.07108504325151443, -0.04895724728703499, 0.022105012089014053, 0.00519839022308588, -0.010209089145064354, 0.036910876631736755, 0.05414630100131035, 0.05123646557331085, -0.06635504215955734, 0.06486642360687256, 0.02087211050093174, -0.06921166926622391, 0.04187197610735893, 0.026369638741016388, 0.04336056485772133, 0.044106848537921906, 0.04993714019656181, -0.01616659201681614, -0.04925931990146637, -0.03643150255084038, 0.0492049939930439, -0.07763552665710449, 0.040213581174612045, -0.005090398248285055, -0.05563517287373543, 0.08514751493930817, 0.015355469658970833, -0.10387051850557327, 0.018668709322810173, 0.07077160477638245, -0.027486123144626617, -0.014717619866132736, 0.004695506766438484, -0.012303460389375687, -0.004724535625427961, -0.05144288018345833, -0.03640333190560341, 0.037046756595373154, -0.008569135330617428, 0.010247169993817806, -0.004085906315594912, -0.07353091239929199, 0.022071469575166702, -0.030118674039840698, -0.10056933015584946, 0.014777586795389652, -0.0227784663438797, 0.007048485334962606, -0.0014997408725321293, 0.023030906915664673, -0.01078913826495409, -0.07683145254850388, 0.019512755796313286, 0.09775645285844803, 0.03273649886250496, -0.036535054445266724, 0.17036518454551697, 0.034777067601680756, -0.057165149599313736, 0.011068180203437805, 0.1110801175236702, -0.07050687819719315, 0.06646030396223068, 0.03532341867685318, -0.0036992349196225405, -0.01897468790411949, -0.0027495413087308407, -0.06805577874183655, -0.012349153868854046, -0.03379824757575989, 0.04108939692378044, 0.10483039915561676, -0.024189557880163193, -0.02803787775337696, -0.08627132326364517, -0.06382598727941513, 0.06018282100558281, -0.06542231142520905, -0.003932671621441841, -0.030126821249723434, 1.0333528734918218e-05, -0.03628602251410484, 0.00844994280487299, -0.01794985681772232, 0.08037517964839935, 0.08628460019826889, -0.0351770743727684, 0.026639962568879128, -0.05487348511815071, -0.009014436975121498, -0.011714656837284565, 0.046275921165943146, 0.005093749612569809, -0.08301075547933578, 0.0, 0.0, -0.0005039895768277347, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0027954732067883015, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.3417782485485077, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.35276928544044495, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]\n"
     ]
    }
   ],
   "source": [
    "from openai import OpenAI\n",
    "\n",
    "\n",
    "def embed_fn(text):\n",
    "    client = OpenAI(base_url=embedding_service_url, api_key=qianfan_api_key)\n",
    "    response = client.embeddings.create(input=[text], model=embedding_model)\n",
    "    return response.data[0].embedding\n",
    "\n",
    "print(embed_fn(\"hello, world!\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 3.2.2.Text Chunk Embedding and Database Ingestion\n",
    "\n",
    "Generate embeddings for each piece of text in the file and add embedding data to the `faiss` database, while storing the text information in the `.jsonl` file."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "id": "4SOhy0lNBhfN"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "embedding:\n",
      "[[ 0.15310541  0.04237456  0.12356884 ...  0.02131988  0.01291032\n",
      "  -0.00546286]\n",
      " [ 0.11144318  0.09504584  0.0567933  ...  0.          0.\n",
      "   0.        ]\n",
      " [ 0.1457673   0.10907461  0.08814128 ...  0.02718093  0.\n",
      "   0.03284565]\n",
      " ...\n",
      " [ 0.10487412 -0.02126087  0.08551556 ...  0.          0.\n",
      "   0.        ]\n",
      " [ 0.13665409  0.0698209   0.04600666 ... -0.03795466  0.\n",
      "   0.        ]\n",
      " [ 0.08973876 -0.02730799  0.00867874 ... -0.0553185   0.\n",
      "   0.04129887]]\n",
      "-------------------------------------------------------------------\n",
      "text_db:\n",
      "{'file_md5s': ['338f7fd3003e6f1d59f8ee92739ed88d'], 'chunks': [{'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，', 'vector_id': 0}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '公平贸易咖啡与有机咖啡是一个不断扩大的市场。\\n传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。', 'vector_id': 1}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。', 'vector_id': 2}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\\n1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\\n1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。', 'vector_id': 3}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\\n在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\\n咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。', 'vector_id': 4}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '香港英文报章在1866年刊登关于coffee shop的报导。1885年香港中文报章以“咖啡”为中文名，此后逐渐成为华语地区普及使用的中文译名。香港在1840年代起有英国人聚居，由于饮食文化的差异，最初被输入到香港的咖啡豆是主要供应西方人饮用，而一般本地华人则不喜欢咖啡苦涩的味道，在早年的香港常有大量从事搬运工作的苦力在码头聚集，为来港的货轮搬运货物，从事体力劳动的苦力比一般华人更容易接触到刚循海路进口的咖啡豆，所以在华人社会中最早有饮用咖啡习惯的群体，却是社会地位低下的码头搬运工。\\n不同地区和民族之间的口味偏好，令咖啡冲泡方式以及调味品的使用多种多样，通常热咖啡添加砂糖、牛奶、奶油、奶精等调味，冷饮咖啡则有更多选择，如酒、薄荷、丁香、柠檬汁等。而不同冲泡和调味方式亦产生出了许多咖啡品类：', 'vector_id': 5}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '土耳其咖啡：是种具有古老历史的咖啡饮品和冲泡方式，而土耳其以外的中东国家以及东南欧皆有流行过此种冲泡方式。土耳其咖啡冲泡好后未经过滤即可直接饮用，土耳其传统上会将土耳其咖啡倒入小瓷杯中慢慢啜饮，而处于悬浊状的咖啡残留有少量咖啡渣亦成为土耳其咖啡独特风味与口感的来源。冲泡土耳其咖啡的方法为将咖啡豆研磨成粉末后装入土耳其壶中，倒入热水并与咖啡末搅拌均匀，再加人豆蔻粉充分搅拌，对土耳其壶加热并充分搅拌。咖啡煮至冒泡后停止加热，待泡沫消失，此时可短暂重复加热2次；或是将三分之一的咖啡先倒入到各个杯子中，壶中剩余的咖啡则再度加热，直到沸腾后倒入之前的杯子里。\\n浓缩咖啡：是一种通过迫使接近沸腾的高压水流通过咖啡末制作而成的咖啡，拿铁咖啡和卡布奇诺、玛琪雅朵等皆是以浓缩咖啡为基本制成的。\\n拿铁咖啡：拿铁咖啡是由浓缩咖啡和热牛奶以1:2的比例冲泡，并加入些许奶泡制成的。也可依需求加上两份浓缩咖啡，意大利语称之为“Double”。', 'vector_id': 6}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '卡布奇诺：卡布奇诺是一种意大利咖啡，是由在浓缩咖啡上倒入奶泡制成，由于咖啡的颜色就像方济嘉布遣会修士深褐色外衣上覆的头巾一样，卡布奇诺也因此得名。其与拿铁咖啡类似，区别仅是卡布奇诺在咖啡、牛奶、奶泡的比例为1:1:1。卡布奇诺咖啡奶泡多，而拿铁咖啡的奶泡少。口味上卡布奇诺咖啡的咖啡味重，而拿铁较为清淡一些，这是因为拿铁的牛奶更多。\\n摩卡咖啡：通常是由三分之一的意式浓缩咖啡和三分之二的奶泡配成，并加入少量巧克力糖浆或速溶巧克力粉。拉夫咖啡是在单杯浓缩咖啡中添加带有少量泡沫（0.5 厘米）的奶油而制成的咖啡。通常与香草糖一起喝用但通常使用糖浆代替香草糖。\\n玛琪雅朵咖啡：在冲泡好的浓缩咖啡上加入鲜奶并倒入一层较薄的奶泡的意大利咖啡。\\n焦糖玛琪雅朵：是一种在浓缩咖啡加入热牛奶和香草，最后淋上焦糖制成的玛琪雅朵咖啡。\\n欧蕾咖啡：是一种咖啡和牛奶的比例为1:1的牛奶咖啡，在冲泡时，需要牛奶壶和咖啡壶从两旁同时注入到咖啡杯。在星巴克则被称为Caffè Misto，以1:1比例的法式压滤咖啡搭配奶泡而成。', 'vector_id': 7}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '美式咖啡：是一种浓缩咖啡以1:5比例加入热水稀释制成的咖啡饮料。冲泡美式咖啡亦可使用意式咖啡机萃取浓缩咖啡，而在咖啡萃取完成后，继续使用咖啡机向浓缩咖啡加入热水稀释到合适比例即可。其浓度随浓缩咖啡的冲泡次数和添加的水量而变化，美式咖啡具有浓缩咖啡风味但却更为柔和。\\n长黑咖啡：是澳大利亚和新西兰常见的一种咖啡，是将双份浓缩咖啡倒入热水中制成的，其恰好与美式咖啡截然相反。长黑咖啡通常使用约100–120毫升的水，但水量可根据个人口味灵活调整。\\n维也纳咖啡：其制作方式为将糖或粗砂糖放入杯内再倒入热咖啡，杯上挤入鲜奶油以及巧克力膏，最终撒上彩色糖粒装饰即可。此种制法可追溯至1683年，当时乌克兰裔波兰军官耶日·弗朗西泽克·库奇茨基开设了奥地利首家咖啡馆并在维也纳开业，其普及了在咖啡中加糖和牛奶的制作和饮用方式。而维也纳咖啡传说是由奥地利马车夫爱因·舒伯纳发明。\\n爱尔兰咖啡：在咖啡中加入威士忌后在其顶部放上奶油。而加入威士忌的爱尔兰咖啡能将咖啡的酸甜味衬托出来。\\n调味咖啡：依据口味的不同在咖啡中加入巧克力、糖浆、果汁、肉桂、肉豆蔻、橘子花等不同调味料。', 'vector_id': 8}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '康宝蓝：康宝蓝是一种在意大利浓缩咖啡上倒入适量奶油的咖啡，并用玻璃咖啡杯盛装，由于鲜奶油具有甜味因此通常无需加糖。\\n白咖啡：起源于马来西亚怡保，其使用经过人造黄油烘培的咖啡豆，冲泡好后加入甜炼乳的饮品。19世纪和20世纪初英国锡矿公司在怡保设立锡矿，而中国移民则在怡保锡矿工作，白咖啡是19世纪中后期移民马来亚的海南人出于华人不习惯咖啡味道而发明。从本质上是一种拿铁咖啡。在美国，白咖啡也指轻度烘培的咖啡豆，使用意式冲煮，具有较强酸味的咖啡。\\n越南咖啡：是一种滴漏咖啡，冲泡时先在盛装咖啡的杯子中倒入炼乳，将滴漏壶置于盛装的杯上，并向滴漏壶加入咖啡末，再以压板压住咖啡末，倒入热水后等待滴漏。越南常用的咖啡豆品种为罗布斯塔，因其带有较重的酸味与苦味以及烘焙时间较长，使得风味较重，因此需要加入炼乳饮用。\\n印度滴漏咖啡：其通常是阿拉比卡咖啡或咖啡公豆制作的；咖啡豆经过深度烘焙、研磨并与菊苣混合，咖啡占混合物的80-90%，其余的为菊苣。菊苣的轻微苦味有助于产生印度滴漏咖啡的风味，传统上使用粗糖或蜂蜜作为甜味剂，但自1900年代中期改为白糖。', 'vector_id': 9}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '皇家咖啡：据说拿破仑在俄法战争时，因遭遇俄国酷寒的冬天，于是命令下属在咖啡里倒入白兰地取暖而发明。其制作方式为，在预热好的咖啡杯中倒入热咖啡，将咖啡匙架在杯缘上，在咖啡匙上放置方糖后淋上白兰地并点火燃烧，火焰熄灭后将咖啡匙放入咖啡搅拌至方糖溶解即可饮用。\\n黑咖啡：是使用滴滤法、渗滤法、虹吸法或加法冲泡的咖啡，在饮用时不添加牛奶、糖等调味品。速溶咖啡是不属于黑咖啡的范围的。\\n希腊法拉沛咖啡：通常由速溶咖啡、糖和牛奶制成的冰咖啡，咖啡中也会倒入奶泡；其口感微甜凉爽，适宜在夏季饮用。\\n阿芙佳朵：是种近乎甜点的冰咖啡，由冰淇淋上加入意大利浓缩咖啡制成。会加入焦糖来增加甜味和促进口感，或加入巧克力酱、可可粉、肉桂粉等。', 'vector_id': 10}]}\n"
     ]
    }
   ],
   "source": [
    "import hashlib\n",
    "import json\n",
    "\n",
    "import faiss\n",
    "import numpy as np\n",
    "\n",
    "\n",
    "def add_embeddings(file_path: str, segments: list[str]) -> bool:\n",
    "    with open(file_path, \"rb\") as f:\n",
    "        file_md5 = hashlib.md5(f.read()).hexdigest()\n",
    "    if file_md5 in text_db[\"file_md5s\"]:\n",
    "        print(f\"File already processed: {file_path} (MD5: {file_md5})\")\n",
    "        return\n",
    "\n",
    "    # Generate embeddings\n",
    "    vectors = []\n",
    "    for i, segment in  enumerate(segments):\n",
    "        vectors.append(embed_fn(segment))\n",
    "    vectors = np.array(vectors)\n",
    "    print(\"embedding:\")\n",
    "    print(vectors)\n",
    "    index.add(vectors.astype('float32'))\n",
    "\n",
    "    start_id = len(text_db[\"chunks\"])\n",
    "    for i, text in enumerate(segments):\n",
    "        text_db[\"chunks\"].append({\n",
    "            \"file_md5\": file_md5,\n",
    "            \"text\": text,\n",
    "            \"vector_id\": start_id + i\n",
    "        })\n",
    "\n",
    "    text_db[\"file_md5s\"].append(file_md5)\n",
    "\n",
    "index = faiss.IndexFlatIP(embedding_dim)\n",
    "text_db = {\n",
    "    \"file_md5s\": [],  # Save file_md5s to avoid duplicates\n",
    "    \"chunks\": []      # Save chunks\n",
    "}\n",
    "add_embeddings(test_file, segments)\n",
    "print(\"-------------------------------------------------------------------\")\n",
    "print(\"text_db:\")\n",
    "print(text_db)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 3.2.3. Knowledge Base Persistence"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "faiss.write_index(index, \"index.faiss\")\n",
    "with open(\"text_db.jsonl\", 'w', encoding='utf-8') as f:\n",
    "    json.dump(text_db, f, ensure_ascii=False, indent=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "ebkGT0ha5Ln3"
   },
   "source": [
    "## 4. Knowledge Base-based Q&A\n",
    "### 4.1. Rewrite the Retrieval Query\n",
    "Determine if retrieval from the knowledge base is needed. If required, rewrite the query for retrieval. A prompt needs to be prepared to guide the model to complete the task and return results in standardized JSON format."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "【当前时间】\n",
      "2025-06-25 14:54:36\n",
      "\n",
      "【对话内容】\n",
      "user:\n",
      "1675 年时，英格兰有多少家咖啡馆？\n",
      "\n",
      "\n",
      "你的任务是根据上面user与assistant的对话内容，理解user意图，改写user的最后一轮对话，以便更高效地从知识库查找相关知识。具体的改写要求如下：\n",
      "1. 如果user的问题包括几个小问题，请将它们分成多个单独的问题。\n",
      "2. 如果user的问题涉及到之前对话的信息，请将这些信息融入问题中，形成一个不需要上下文就可以理解的完整问题。\n",
      "3. 如果user的问题是在比较或关联多个事物时，先将其拆分为单个事物的问题，例如‘A与B比起来怎么样’，拆分为：‘A怎么样’以及‘B怎么样’。\n",
      "4. 如果user的问题中描述事物的限定词有多个，请将多个限定词拆分成单个限定词。\n",
      "5. 如果user的问题具有**时效性（需要包含当前时间信息，才能得到正确的回复）**的时候，需要将当前时间信息添加到改写的query中；否则不加入当前时间信息。\n",
      "6. 只在**确有必要**的情况下改写，不需要改写时query输出[]。输出不超过 5 个改写问题，不要为了凑满数量而输出冗余问题。\n",
      "\n",
      "【输出格式】只输出 JSON ，不要给出多余内容\n",
      "```json\n",
      "{\n",
      "\"query\": [\"改写问题1\", \"改写问题2\"...]\n",
      "}```\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import textwrap\n",
    "from datetime import datetime\n",
    "\n",
    "QUERY_REWRITE_PROMPT = textwrap.dedent(\"\"\"\\\n",
    "    【当前时间】\n",
    "    {TIMESTAMP}\n",
    "\n",
    "    【对话内容】\n",
    "    {CONVERSATION}\n",
    "\n",
    "    你的任务是根据上面user与assistant的对话内容，理解user意图，改写user的最后一轮对话，以便更高效地从知识库查找相关知识。具体的改写要求如下：\n",
    "    1. 如果user的问题包括几个小问题，请将它们分成多个单独的问题。\n",
    "    2. 如果user的问题涉及到之前对话的信息，请将这些信息融入问题中，形成一个不需要上下文就可以理解的完整问题。\n",
    "    3. 如果user的问题是在比较或关联多个事物时，先将其拆分为单个事物的问题，例如‘A与B比起来怎么样’，拆分为：‘A怎么样’以及‘B怎么样’。\n",
    "    4. 如果user的问题中描述事物的限定词有多个，请将多个限定词拆分成单个限定词。\n",
    "    5. 如果user的问题具有**时效性（需要包含当前时间信息，才能得到正确的回复）**的时候，需要将当前时间信息添加到改写的query中；否则不加入当前时间信息。\n",
    "    6. 只在**确有必要**的情况下改写，不需要改写时query输出[]。输出不超过 5 个改写问题，不要为了凑满数量而输出冗余问题。\n",
    "\n",
    "    【输出格式】只输出 JSON ，不要给出多余内容\n",
    "    ```json\n",
    "    {{\n",
    "    \"query\": [\"改写问题1\", \"改写问题2\"...]\n",
    "    }}```\n",
    "    \"\"\"\n",
    ")\n",
    "\n",
    "query = \"1675 年时，英格兰有多少家咖啡馆？\"\n",
    "conversation_str = f\"user:\\n{query}\\n\"\n",
    "search_info_input = QUERY_REWRITE_PROMPT.format(\n",
    "    TIMESTAMP=datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n",
    "    CONVERSATION=conversation_str\n",
    ")\n",
    "print(search_info_input)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Call the model interface for judgment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'id': 'chatcmpl-1a50b468-f25f-4d48-91d8-ff48c86e86a1', 'choices': [{'finish_reason': 'stop', 'index': 0, 'logprobs': None, 'message': {'content': '```json\\n{\\n    \"query\": [\"1675年 英格兰咖啡馆数量\"]\\n}\\n```</s></s>', 'refusal': None, 'role': 'assistant', 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': None, 'reasoning_content': None}}], 'created': 1750834480, 'model': 'default', 'object': 'chat.completion', 'service_tier': None, 'system_fingerprint': None, 'usage': {'completion_tokens': 26, 'prompt_tokens': 330, 'total_tokens': 356, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}}\n"
     ]
    }
   ],
   "source": [
    "judge_search_messages = [{\"role\": \"user\", \"content\": search_info_input}]\n",
    "\n",
    "client = OpenAI(base_url=host_url, api_key=model_api_key)\n",
    "search_info_res = client.chat.completions.create(\n",
    "    model=\"default\",\n",
    "    messages=judge_search_messages\n",
    ")\n",
    "\n",
    "search_info_res = search_info_res.model_dump()\n",
    "print(search_info_res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "parse the model's results to json format.\n",
    "- `query`: A list of queries to be retrieved."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'query': ['1675年 英格兰咖啡馆数量']}\n"
     ]
    }
   ],
   "source": [
    "import re\n",
    "\n",
    "search_info_res = search_info_res[\"choices\"][0][\"message\"][\"content\"]\n",
    "json_match = re.search(r'```json\\n(.*?)\\n```', search_info_res, re.DOTALL)\n",
    "json_str = json_match.group(1)\n",
    "search_info_res = json.loads(json_str)\n",
    "\n",
    "print(search_info_res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2. Knowledge Base Retrieval\n",
    "\n",
    "If the previous step determines that online search is needed, retrieve relevant texts from the knowledge base.\n",
    "\n",
    "The retrieval process is as follows:\n",
    "\n",
    "1. Query Retrieval: Embed each query, use FAISS index to retrieve the `top_k` results with highest similarity, and collect all result indices into a unified list.\n",
    "\n",
    "2. Result Deduplication: Deduplicate and sort the collected indices to eliminate duplicate retrieval results, preparing an ordered index sequence for subsequent processing.\n",
    "\n",
    "3. Context Expansion: For each target index, dynamically determine the boundary range of its source file, expand the context window (±context_size) within file boundary limits, and generate a new index set with context included.\n",
    "\n",
    "4. Continuous Chunks Merging: Sort the expanded indices by physical position, detect continuous index sequences belonging to the same file, and group them into continuous text chunk units.\n",
    "\n",
    "5. Result Generation: Merge all text content within blocks and output the complete text with structured markup."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Retrieved indices: [3, 2, 4]\n",
      "Unique indices after deduplication: [2, 3, 4]\n",
      "Merged chunk range: 0-6\n",
      "\n",
      "段落1:\n",
      "咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，\n",
      "公平贸易咖啡与有机咖啡是一个不断扩大的市场。\n",
      "传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。\n",
      "咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。\n",
      "同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\n",
      "1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\n",
      "1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。\n",
      "18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\n",
      "在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\n",
      "咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。\n",
      "香港英文报章在1866年刊登关于coffee shop的报导。1885年香港中文报章以“咖啡”为中文名，此后逐渐成为华语地区普及使用的中文译名。香港在1840年代起有英国人聚居，由于饮食文化的差异，最初被输入到香港的咖啡豆是主要供应西方人饮用，而一般本地华人则不喜欢咖啡苦涩的味道，在早年的香港常有大量从事搬运工作的苦力在码头聚集，为来港的货轮搬运货物，从事体力劳动的苦力比一般华人更容易接触到刚循海路进口的咖啡豆，所以在华人社会中最早有饮用咖啡习惯的群体，却是社会地位低下的码头搬运工。\n",
      "不同地区和民族之间的口味偏好，令咖啡冲泡方式以及调味品的使用多种多样，通常热咖啡添加砂糖、牛奶、奶油、奶精等调味，冷饮咖啡则有更多选择，如酒、薄荷、丁香、柠檬汁等。而不同冲泡和调味方式亦产生出了许多咖啡品类：\n",
      "土耳其咖啡：是种具有古老历史的咖啡饮品和冲泡方式，而土耳其以外的中东国家以及东南欧皆有流行过此种冲泡方式。土耳其咖啡冲泡好后未经过滤即可直接饮用，土耳其传统上会将土耳其咖啡倒入小瓷杯中慢慢啜饮，而处于悬浊状的咖啡残留有少量咖啡渣亦成为土耳其咖啡独特风味与口感的来源。冲泡土耳其咖啡的方法为将咖啡豆研磨成粉末后装入土耳其壶中，倒入热水并与咖啡末搅拌均匀，再加人豆蔻粉充分搅拌，对土耳其壶加热并充分搅拌。咖啡煮至冒泡后停止加热，待泡沫消失，此时可短暂重复加热2次；或是将三分之一的咖啡先倒入到各个杯子中，壶中剩余的咖啡则再度加热，直到沸腾后倒入之前的杯子里。\n",
      "浓缩咖啡：是一种通过迫使接近沸腾的高压水流通过咖啡末制作而成的咖啡，拿铁咖啡和卡布奇诺、玛琪雅朵等皆是以浓缩咖啡为基本制成的。\n",
      "拿铁咖啡：拿铁咖啡是由浓缩咖啡和热牛奶以1:2的比例冲泡，并加入些许奶泡制成的。也可依需求加上两份浓缩咖啡，意大利语称之为“Double”。\n",
      "\n"
     ]
    }
   ],
   "source": [
    "def search_with_context(query_list: list, context_size: int=2,) -> str:\n",
    "    # Step 1: Retrieve top_k results for each query and collect all indices\n",
    "    all_indices = []\n",
    "    for query in query_list:\n",
    "        query_vector = np.array([embed_fn(query)]).astype('float32')\n",
    "        _, indices = index.search(query_vector, top_k)\n",
    "        all_indices.extend(indices[0].tolist())\n",
    "\n",
    "    # Step 2: Remove duplicate indices\n",
    "    unique_indices = sorted(set(all_indices))\n",
    "    print(f\"Retrieved indices: {all_indices}\")\n",
    "    print(f\"Unique indices after deduplication: {unique_indices}\")\n",
    "\n",
    "    # Step 3: Expand each index with context (within same file boundaries)\n",
    "    expanded_indices = set()\n",
    "    file_boundaries = {}  # {file_md5: (start_idx, end_idx)}\n",
    "    for target_idx in unique_indices:\n",
    "        target_chunk = text_db[\"chunks\"][target_idx]\n",
    "        target_file_md5 = target_chunk[\"file_md5\"]\n",
    "\n",
    "        if target_file_md5 not in file_boundaries:\n",
    "            file_start = target_idx\n",
    "            while file_start > 0 and text_db[\"chunks\"][file_start - 1][\"file_md5\"] == target_file_md5:\n",
    "                file_start -= 1\n",
    "            file_end = target_idx\n",
    "            while (file_end < len(text_db[\"chunks\"]) - 1 and\n",
    "                text_db[\"chunks\"][file_end + 1][\"file_md5\"] == target_file_md5):\n",
    "                file_end += 1\n",
    "        else:\n",
    "            file_start, file_end = file_boundaries[target_file_md5]\n",
    "\n",
    "        # Calculate context range within file boundaries\n",
    "        start = max(file_start, target_idx - context_size)\n",
    "        end = min(file_end, target_idx + context_size)\n",
    "\n",
    "        for pos in range(start, end + 1):\n",
    "            expanded_indices.add(pos)\n",
    "\n",
    "    # Step 4: Sort and merge continue chunks\n",
    "    sorted_indices = sorted(expanded_indices)\n",
    "    groups = []\n",
    "    current_group = [sorted_indices[0]]\n",
    "    for i in range(1, len(sorted_indices)):\n",
    "        if (sorted_indices[i] == sorted_indices[i-1] + 1 and\n",
    "            text_db[\"chunks\"][sorted_indices[i]][\"file_md5\"] ==\n",
    "            text_db[\"chunks\"][sorted_indices[i-1]][\"file_md5\"]):\n",
    "            current_group.append(sorted_indices[i])\n",
    "        else:\n",
    "            groups.append(current_group)\n",
    "            current_group = [sorted_indices[i]]\n",
    "    groups.append(current_group)\n",
    "\n",
    "    # Step 5: Create merged text for each group\n",
    "    result = \"\"\n",
    "    for idx, group in enumerate(groups):\n",
    "        result += f\"\\n段落{idx + 1}:\\n\"\n",
    "        for idx in group:\n",
    "            result += text_db[\"chunks\"][idx][\"text\"] + \"\\n\"\n",
    "        print(f\"Merged chunk range: {group[0]}-{group[-1]}\")\n",
    "\n",
    "    return result\n",
    "\n",
    "relevant_passages = \"\"\n",
    "if search_info_res.get(\"query\", []):\n",
    "    relevant_passages = search_with_context(search_info_res[\"query\"])\n",
    "print(relevant_passages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3. Generate Final Answer\n",
    "#### 4.3.1. Model Input\n",
    "The model's input is a message list that represents the context history of the conversation. Each message is a dictionary containing the following fields:\n",
    "- `role`: Represents the role of the message sender, which can be:\n",
    "    - `user`: User message, indicating user input\n",
    "    - `assistant`: Model message, indicating the model's reply\n",
    "- `content`: Specific text content\n",
    "\n",
    "The input has the following characteristics:\n",
    "- Knowledge Base Search: Splice the search results into `ANSWER_PROMPT` and provide them to the model as context.\n",
    "- Multiple Rounds of Dialogue: Supporting the preservation of historical dialogue context"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "id": "mlpDRG3cVvQE"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[{'role': 'user', 'content': \"你是阅读理解问答专家。\\n\\n【文档知识】\\n\\n段落1:\\n咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，\\n公平贸易咖啡与有机咖啡是一个不断扩大的市场。\\n传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。\\n咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。\\n同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\\n1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\\n1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。\\n18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\\n在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\\n咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。\\n香港英文报章在1866年刊登关于coffee shop的报导。1885年香港中文报章以“咖啡”为中文名，此后逐渐成为华语地区普及使用的中文译名。香港在1840年代起有英国人聚居，由于饮食文化的差异，最初被输入到香港的咖啡豆是主要供应西方人饮用，而一般本地华人则不喜欢咖啡苦涩的味道，在早年的香港常有大量从事搬运工作的苦力在码头聚集，为来港的货轮搬运货物，从事体力劳动的苦力比一般华人更容易接触到刚循海路进口的咖啡豆，所以在华人社会中最早有饮用咖啡习惯的群体，却是社会地位低下的码头搬运工。\\n不同地区和民族之间的口味偏好，令咖啡冲泡方式以及调味品的使用多种多样，通常热咖啡添加砂糖、牛奶、奶油、奶精等调味，冷饮咖啡则有更多选择，如酒、薄荷、丁香、柠檬汁等。而不同冲泡和调味方式亦产生出了许多咖啡品类：\\n土耳其咖啡：是种具有古老历史的咖啡饮品和冲泡方式，而土耳其以外的中东国家以及东南欧皆有流行过此种冲泡方式。土耳其咖啡冲泡好后未经过滤即可直接饮用，土耳其传统上会将土耳其咖啡倒入小瓷杯中慢慢啜饮，而处于悬浊状的咖啡残留有少量咖啡渣亦成为土耳其咖啡独特风味与口感的来源。冲泡土耳其咖啡的方法为将咖啡豆研磨成粉末后装入土耳其壶中，倒入热水并与咖啡末搅拌均匀，再加人豆蔻粉充分搅拌，对土耳其壶加热并充分搅拌。咖啡煮至冒泡后停止加热，待泡沫消失，此时可短暂重复加热2次；或是将三分之一的咖啡先倒入到各个杯子中，壶中剩余的咖啡则再度加热，直到沸腾后倒入之前的杯子里。\\n浓缩咖啡：是一种通过迫使接近沸腾的高压水流通过咖啡末制作而成的咖啡，拿铁咖啡和卡布奇诺、玛琪雅朵等皆是以浓缩咖啡为基本制成的。\\n拿铁咖啡：拿铁咖啡是由浓缩咖啡和热牛奶以1:2的比例冲泡，并加入些许奶泡制成的。也可依需求加上两份浓缩咖啡，意大利语称之为“Double”。\\n\\n\\n你的任务是根据对话内容，理解用户需求，参考文档知识回答用户问题，知识参考详细原则如下：\\n- 对于同一信息点，如文档知识与模型通用知识均可支撑，应优先以文档知识为主，并对信息进行验证和综合。\\n- 如果文档知识不足或信息冲突，必须指出“根据资料无法确定”或“不同资料存在矛盾”，不得引入文档知识与通识之外的主观推测。\\n\\n同时，回答问题需要综合考虑规则要求中的各项内容，详细要求如下：\\n【规则要求】\\n* 回答问题时，应优先参考与问题紧密相关的文档知识，不要在答案中引入任何与问题无关的文档内容。\\n* 回答中不可以让用户知道你查询了相关文档。\\n* 回复答案不要出现'根据文档知识'，'根据当前时间'等表述。\\n* 论述突出重点内容，以分点条理清晰的结构化格式输出。\\n\\n【当前时间】\\n2025-06-25 14:54:51\\n\\n【对话内容】\\nuser:\\n1675 年时，英格兰有多少家咖啡馆？\\n\\n\\n直接输出回复内容即可。\\n\"}]\n"
     ]
    }
   ],
   "source": [
    "ANSWER_PROMPT = textwrap.dedent(\n",
    "    \"\"\"\\\n",
    "    你是阅读理解问答专家。\n",
    "\n",
    "    【文档知识】\n",
    "    {DOC_CONTENT}\n",
    "\n",
    "    你的任务是根据对话内容，理解用户需求，参考文档知识回答用户问题，知识参考详细原则如下：\n",
    "    - 对于同一信息点，如文档知识与模型通用知识均可支撑，应优先以文档知识为主，并对信息进行验证和综合。\n",
    "    - 如果文档知识不足或信息冲突，必须指出“根据资料无法确定”或“不同资料存在矛盾”，不得引入文档知识与通识之外的主观推测。\n",
    "\n",
    "    同时，回答问题需要综合考虑规则要求中的各项内容，详细要求如下：\n",
    "    【规则要求】\n",
    "    * 回答问题时，应优先参考与问题紧密相关的文档知识，不要在答案中引入任何与问题无关的文档内容。\n",
    "    * 回答中不可以让用户知道你查询了相关文档。\n",
    "    * 回复答案不要出现'根据文档知识'，'根据当前时间'等表述。\n",
    "    * 论述突出重点内容，以分点条理清晰的结构化格式输出。\n",
    "\n",
    "    【当前时间】\n",
    "    {TIMESTAMP}\n",
    "\n",
    "    【对话内容】\n",
    "    {CONVERSATION}\n",
    "\n",
    "    直接输出回复内容即可。\n",
    "    \"\"\"\n",
    ")\n",
    "\n",
    "if search_info_res.get(\"query\", []):\n",
    "    input = ANSWER_PROMPT.format(\n",
    "        DOC_CONTENT=relevant_passages,\n",
    "        TIMESTAMP=datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n",
    "        CONVERSATION=conversation_str\n",
    "    )\n",
    "else:\n",
    "    input = query\n",
    "\n",
    "messages = [{\"role\": \"user\", \"content\": input}]\n",
    "print(messages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "qmdYdoIHcEc_"
   },
   "source": [
    "#### 4.3.2. Non-Streaming Request\n",
    "##### Request Model\n",
    "When sending a request to the API, the following main parameters need to be considered:\n",
    "- `messages` (must): List of conversation messages\n",
    "- `max_tokens` (optional): configuration parameter for maximum number of generated tokens\n",
    "- `temperature` (optional): configuration parameter for controlling randomness in generated results\n",
    "- `top_p` (optional): configuration parameter for nucleus sampling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "id": "m30avD9cfQQ-"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'id': 'chatcmpl-546e2fc9-9ec1-4d3d-bd2b-c800bb616556', 'choices': [{'finish_reason': 'stop', 'index': 0, 'logprobs': None, 'message': {'content': '1675年时，英格兰有3000多家咖啡馆。</s></s>', 'refusal': None, 'role': 'assistant', 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': None, 'reasoning_content': None}}], 'created': 1750763172, 'model': 'default', 'object': 'chat.completion', 'service_tier': None, 'system_fingerprint': None, 'usage': {'completion_tokens': 18, 'prompt_tokens': 1978, 'total_tokens': 1996, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}}\n"
     ]
    }
   ],
   "source": [
    "client = OpenAI(base_url=host_url, api_key=model_api_key)\n",
    "response = client.chat.completions.create(\n",
    "    model=\"default\",\n",
    "    messages=messages,\n",
    "    temperature=1.0,\n",
    "    max_tokens=2048,\n",
    "    top_p=0.7\n",
    ")\n",
    "response = response.model_dump()\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Model Output\n",
    "- `content`: Final answer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "id": "COBhn6J9S_xI"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1675年时，英格兰有3000多家咖啡馆。</s></s>\n"
     ]
    }
   ],
   "source": [
    "content = response[\"choices\"][0][\"message\"][\"content\"]\n",
    "print(content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 4.3.3. Streaming Request\n",
    "##### Request Model\n",
    "When sending a request to the API, the following main parameters need to be considered:\n",
    "- `messages` (must): List of conversation messages\n",
    "- `max_tokens` (optional): configuration parameter for maximum number of generated tokens\n",
    "- `temperature` (optional): configuration parameter for controlling randomness in generated results\n",
    "- `top_p` (optional): configuration parameter for nucleus sampling\n",
    "- `stream` (optional): configuration parameter for enabling/disabling streamed return"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[{'id': 'chatcmpl-bf801edc-abce-496a-8329-0994ec9c1a5b', 'choices': [{'delta': {'content': '', 'function_call': None, 'refusal': None, 'role': 'assistant', 'tool_calls': None, 'reasoning_content': ''}, 'finish_reason': None, 'index': 0, 'logprobs': None}], 'created': 1750411440, 'model': 'default', 'object': 'chat.completion.chunk', 'service_tier': None, 'system_fingerprint': None, 'usage': None}, {'id': 'chatcmpl-bf801edc-abce-496a-8329-0994ec9c1a5b', 'choices': [{'delta': {'content': '1', 'function_call': None, 'refusal': None, 'role': None, 'tool_calls': None, 'token_ids': [4], 'reasoning_content': None}, 'finish_reason': None, 'index': 0, 'logprobs': None, 'arrival_time': 0.12418651580810547}], 'created': 1750411440, 'model': 'default', 'object': 'chat.completion.chunk', 'service_tier': None, 'system_fingerprint': None, 'usage': None}, {'id': 'chatcmpl-bf801edc-abce-496a-8329-0994ec9c1a5b', 'choices': [{'delta': {'content': '6', 'function_call': None, 'refusal': None, 'role': None, 'tool_calls': None, 'token_ids': [9], 'reasoning_content': None}, 'finish_reason': None, 'index': 0, 'logprobs': None, 'arrival_time': 0.14658284187316895}], 'created': 1750411440, 'model': 'default', 'object': 'chat.completion.chunk', 'service_tier': None, 'system_fingerprint': None, 'usage': None}]\n"
     ]
    }
   ],
   "source": [
    "response = client.chat.completions.create(\n",
    "    model=\"default\",\n",
    "    messages=messages,\n",
    "    temperature=1.0,\n",
    "    max_tokens=2048,\n",
    "    top_p=0.7,\n",
    "    stream=True\n",
    ")\n",
    "response_stream = []\n",
    "for chunk in response:\n",
    "    if not chunk.choices:\n",
    "        continue\n",
    "    response_stream.append(chunk.model_dump())\n",
    "\n",
    "print(response_stream[:3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Model Output\n",
    "- `content`: Final answer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1675年时，英格兰有3000多家咖啡馆。</s>\n"
     ]
    }
   ],
   "source": [
    "content = \"\"\n",
    "for res in response_stream:\n",
    "    content += res[\"choices\"][0][\"delta\"][\"content\"]\n",
    "print(content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "colab": {
   "name": "Talk_to_documents_with_embeddings.ipynb",
   "toc_visible": true
  },
  "google": {
   "image_path": "/site-assets/images/share.png",
   "keywords": [
    "examples",
    "googleai",
    "samplecode",
    "python",
    "embed"
   ]
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
